#include <cassert>

#include <Windows.h>

#include "Input.hpp"

#include "Debug.hpp"

CInput::CInput(EventHandler& eventHandler) : eventHandler(eventHandler)
{
	keyInput.reserve(2);
	mouseInput.reserve(2);
	for(size_t i = 0; i < 2; ++i)
	{
		keyInput.push_back(new KeyInput);
		keyInput[i]->keyStates.reserve(256);
		for(int j = 0; j < 256; ++j)
		{
			ButtonState* keyState = new ButtonState();
			keyInput[i]->keyStates.push_back(keyState);
		}

		mouseInput.push_back(new MouseInput);
		mouseInput[i]->mouseStates.reserve(5);
		for(int j = 0; j < 5; ++j)
		{
			ButtonState* keyState = new ButtonState();
			mouseInput[i]->mouseStates.push_back(keyState);
		}
	}

	bufIdx = 0;

	thr = std::thread(std::bind(&CInput::InputThread, this));
}

CInput::~CInput()
{
	::PostMessage(hWnd, WM_DESTROY, 0, 0);

	::UnregisterClassW(L"BattleBots_Input", ::GetModuleHandle(nullptr));

	inputDevices[0].dwFlags = RIDEV_REMOVE;
	inputDevices[0].hwndTarget = nullptr;

	inputDevices[1].dwFlags = RIDEV_REMOVE;
	inputDevices[1].hwndTarget = nullptr;

	if(::RegisterRawInputDevices(inputDevices, 2, sizeof(RAWINPUTDEVICE)) == FALSE)
		throw std::exception("Could not unregister raw input devices");

	thr.join();
}

LRESULT CALLBACK CInput::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	CInput* input = nullptr;

	if(uMsg != WM_CREATE)
	{
		input = reinterpret_cast<CInput*>(::GetWindowLongPtr(hWnd, 0));
		if(input != nullptr)
			return input->MsgHandler(hWnd, uMsg, wParam, lParam);
	}
	else
	{
		input = static_cast<CInput*>(((LPCREATESTRUCT)lParam)->lpCreateParams);
		assert(input);

		::SetWindowLongPtr(hWnd, 0, (LONG_PTR)input);
	}

	return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}

LRESULT CALLBACK CInput::MsgHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch(uMsg)
	{
	case WM_INPUT:
		{
			unsigned size;
			if(::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER)) != FALSE)
			{
				DWORD err = ::GetLastError();
				break;
			}

			if(size == 0)
				break;

			static RAWINPUT buf = { 0 };

			unsigned sizeRead = ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &buf, &size, sizeof(RAWINPUTHEADER));
			assert(sizeRead == size && size <= sizeof(RAWINPUT));

			switch(buf.header.dwType)
			{
			case RIM_TYPEKEYBOARD:
				{
					if((buf.data.keyboard.Flags & RI_KEY_BREAK) == RI_KEY_BREAK)
					{
						keyInput[bufIdx]->keyStates[buf.data.keyboard.VKey]->wasDown = true;
						keyInput[bufIdx]->keyStates[buf.data.keyboard.VKey]->isDown = false;
					}
					else
						keyInput[bufIdx]->keyStates[buf.data.keyboard.VKey]->isDown = true;
				}
				break;

			case RIM_TYPEMOUSE:
				{
					// X/Y Movement
					if((buf.data.mouse.usFlags & MOUSE_MOVE_RELATIVE) == MOUSE_MOVE_RELATIVE)
					{
						mouseInput[bufIdx]->mouseX = buf.data.mouse.lLastX;
						mouseInput[bufIdx]->mouseY = buf.data.mouse.lLastY;
					}
					else if((buf.data.mouse.usFlags & MOUSE_MOVE_RELATIVE) == MOUSE_MOVE_RELATIVE)
					{
						mouseInput[bufIdx]->mouseX += buf.data.mouse.lLastX;
						mouseInput[bufIdx]->mouseY += buf.data.mouse.lLastY;
					}

					// Wheel Movement
					if((buf.data.mouse.usButtonFlags & RI_MOUSE_WHEEL) == RI_MOUSE_WHEEL)
						mouseInput[bufIdx]->wheelDelta = buf.data.mouse.usButtonData;

					// Button states
					if((buf.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_1_DOWN) == RI_MOUSE_BUTTON_1_DOWN)
						mouseInput[bufIdx]->mouseStates[0]->isDown = true;
					else if((buf.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_1_UP) == RI_MOUSE_BUTTON_1_UP)
					{
						mouseInput[bufIdx]->mouseStates[0]->wasDown = true;
						mouseInput[bufIdx]->mouseStates[0]->isDown = false;
					}

					if((buf.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_2_DOWN) == RI_MOUSE_BUTTON_2_DOWN)
						mouseInput[bufIdx]->mouseStates[1]->isDown = true;
					else if((buf.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_2_UP) == RI_MOUSE_BUTTON_2_UP)
					{
						mouseInput[bufIdx]->mouseStates[1]->wasDown = true;
						mouseInput[bufIdx]->mouseStates[1]->isDown = false;
					}

					if((buf.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_3_DOWN) == RI_MOUSE_BUTTON_3_DOWN)
						mouseInput[bufIdx]->mouseStates[2]->isDown = true;
					else if((buf.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_3_UP) == RI_MOUSE_BUTTON_3_UP)
					{
						mouseInput[bufIdx]->mouseStates[2]->wasDown = true;
						mouseInput[bufIdx]->mouseStates[2]->isDown = false;
					}

					if((buf.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_DOWN) == RI_MOUSE_BUTTON_4_DOWN)
						mouseInput[bufIdx]->mouseStates[3]->isDown = true;
					else if((buf.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_UP) == RI_MOUSE_BUTTON_4_UP)
					{
						mouseInput[bufIdx]->mouseStates[3]->wasDown = true;
						mouseInput[bufIdx]->mouseStates[3]->isDown = false;
					}

					if((buf.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_DOWN) == RI_MOUSE_BUTTON_5_DOWN)
						mouseInput[bufIdx]->mouseStates[4]->isDown = true;
					else if((buf.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_UP) == RI_MOUSE_BUTTON_5_UP)
					{
						mouseInput[bufIdx]->mouseStates[4]->wasDown = true;
						mouseInput[bufIdx]->mouseStates[4]->isDown = false;
					}
				}
				break;

			case RIM_TYPEHID:
				{
				}
				break;
			}
		}
		return 0;

	case WM_DESTROY:
		// clear our entry so it is no longer accessed in CInput::WndProc
		::SetWindowLongPtr(hWnd, 0, (LONG_PTR)nullptr);
		::PostQuitMessage(0);
		return 0;
	}

	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

void CInput::WindowCreate()
{
	try
	{
		HINSTANCE hInstance = ::GetModuleHandle(nullptr);

		WNDCLASSEX wcInput = { 0 };
		wcInput.cbSize			= sizeof(WNDCLASSEX);
		wcInput.style			= 0;
		wcInput.lpfnWndProc		= (WNDPROC)CInput::WndProc;
		wcInput.cbClsExtra		= 0;
		wcInput.cbWndExtra		= sizeof(CInput*);
		wcInput.hInstance		= hInstance;
		wcInput.hCursor			= nullptr;
		wcInput.hbrBackground	= nullptr;
		wcInput.lpszMenuName	= nullptr;
		wcInput.lpszClassName	= L"BattleBots Input";
		wcInput.hIcon = nullptr;

		ATOM classAtom = 0;
		WNDCLASSEX wc2Input = { 0 };
		if(!::GetClassInfoExW(hInstance, L"BattleBots Input", &wc2Input))
			if(!(classAtom = ::RegisterClassEx(&wcInput)))
				throw "Failed to register input window class.";

		hWnd = ::CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, 
			L"BattleBots Input", L"BattleBots Input",
			WS_OVERLAPPEDWINDOW,
			0, 0, 0, 0,
			HWND_MESSAGE, nullptr, nullptr, this);

		if(hWnd == nullptr)
			throw std::exception("Could not create input dummy window");
	}
	catch(const std::exception& ex)
	{
		throw ex;
	}
}

void CInput::RegisterDevices()
{
	// Mouse HID
	inputDevices[0].usUsagePage = 0x01;
	inputDevices[0].usUsage = 0x02;
	inputDevices[0].dwFlags = 0; //RIDEV_NOLEGACY
	inputDevices[0].hwndTarget = hWnd;

	// Keyboard HID
	inputDevices[1].usUsagePage = 0x01;
	inputDevices[1].usUsage = 0x06;
	inputDevices[1].dwFlags = 0;
	inputDevices[1].hwndTarget = hWnd;

#if 0
	// Game pad
	device->usUsagePage = 0x01;
	device->usUsage = 0x05;
	device->dwFlags = RIDEV_NOLEGACY;
	device->hwndTarget = this->hWnd;
	inputDevices.push_back(device);

	// Joystick
	device->usUsagePage = 0x01;
	device->usUsage = 0x04;
	device->dwFlags = RIDEV_NOLEGACY;
	device->hwndTarget = this->hWnd;
	inputDevices.push_back(device);
#endif
	if(::RegisterRawInputDevices(inputDevices, 2, sizeof(RAWINPUTDEVICE)) == FALSE)
	{
		DWORD err = ::GetLastError();
		throw std::exception("could not register raw input devices.");
	}
}

HWND CInput::GetWindowHandle() const
{
	return hWnd;
}

void CInput::SetWindowHandle(HWND handle)
{
	hWnd = handle;
}

int CInput::InputThread()
{
	WindowCreate();

	RegisterDevices();

	//eventHandler.AddListener(EventType::Shutdown, std::bind(&CInput::OnShutdown, this, std::placeholders::_1));

	MSG msg = { 0 };
	BOOL ret = FALSE;

	while(ret = ::GetMessage(&msg, hWnd, 0, 0))
	{
		::TranslateMessage(&msg); 
		::DispatchMessage(&msg);
	}

	return 0;
}

size_t CInput::SwapInputBuffers()
{
	// Swap current active buffer index
	bufIdx = bufIdx ? 0 : 1;

	// Clear buffers before accumulating additional input
	ClearKeyInput();
	ClearMouseInput();

	return bufIdx;
}

KeyInput* CInput::GetKeyInput()
{
	return keyInput[bufIdx];
}

MouseInput* CInput::GetMouseInput()
{
	return mouseInput[bufIdx];
};

void CInput::ClearKeyInput()
{
	for(auto it(keyInput[bufIdx]->keyStates.begin()), end(keyInput[bufIdx]->keyStates.end()); it != end; ++it)
	{
		(*it)->isDown = false;
		(*it)->wasDown = false;
	}
}

void CInput::ClearMouseInput()
{
	mouseInput[bufIdx]->mouseX = 0;
	mouseInput[bufIdx]->mouseY = 0;
	mouseInput[bufIdx]->wheelDelta = 0;
	for(auto it(mouseInput[bufIdx]->mouseStates.begin()), end(mouseInput[bufIdx]->mouseStates.end()); it != end; ++it)
	{
		(*it)->isDown = false;
		(*it)->wasDown = false;
	}
}
